package com.tyros.resource.strategy;

import cn.hutool.core.lang.UUID;
import cn.hutool.crypto.SecureUtil;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.tyros.common.constant.BaseMessage;
import com.tyros.common.exception.BaseException;
import com.tyros.common.utils.HttpContextUtils;
import com.tyros.common.utils.LogUtils;
import com.tyros.resource.config.property.StorageProperties;
import com.tyros.resource.constant.DataSourceConst;
import com.tyros.resource.constant.ResourceMessage;
import com.tyros.resource.modules.file.dto.FileDTO;
import com.tyros.resource.modules.file.dto.request.DeleteRequestDTO;
import com.tyros.resource.modules.file.dto.request.DownloadRequestDTO;
import com.tyros.resource.modules.file.dto.request.UploadRequestDTO;
import com.tyros.resource.modules.file.service.FileService;
import com.tyros.resource.tool.FileNameTools;
import com.tyros.resource.tool.RangeParser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import javax.activation.MimetypesFileTypeMap;
import javax.annotation.Resource;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.net.URLEncoder;
import java.util.Objects;

/**
 * @author zhangshuaiyin
 * @date 2022/2/20 11:04
 */
@Slf4j
public abstract class AbstractFileStrategy implements FileStrategy {
    @Resource
    private FileService fileService;
    @Resource
    private StorageProperties storageProperties;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public FileDTO upload(UploadRequestDTO requestDTO) throws Exception {
        LogUtils.info(log, "{} 模式存储", storageProperties.getType());
        MultipartFile file = requestDTO.getFile();
        FileDTO fileDTO = new FileDTO();
        // 文件唯一条件
        fileDTO.setMd5(SecureUtil.md5(file.getInputStream()))
                .setSha256(SecureUtil.sha256(file.getInputStream()))
                .setStorageType(Objects.requireNonNull(storageProperties.getType()).getCode())
                .setOriginalName(file.getOriginalFilename());

        // 1. 文件上传检查
        FileDTO uniqueRecord = this.check(file, fileDTO);
        if (Objects.nonNull(uniqueRecord)) {
            // 如果文件存在且被删除，恢复该文件
            if (DataSourceConst.DELETED.equals(uniqueRecord.getDeleted())) {
                fileService.logicInsert(uniqueRecord);
                // 2. 文件磁盘/云服务上传保存
                this.upload(file, uniqueRecord);
            }
            // 没被删除，直接返回
            return uniqueRecord;
        }

        String fileName = UUID.fastUUID() + StringPool.DOT + FileNameTools.extName(file.getOriginalFilename());

        fileDTO.setName(fileName)
                .setExtension(FileNameTools.extName(file.getOriginalFilename()))
                .setSize(file.getSize()).setObjectKey(FileNameTools.defaultPath() + fileName)
                .setDeleted(DataSourceConst.NON_DELETED);

        // 2. 文件磁盘/云服务上传保存
        this.upload(file, fileDTO);

        // 3. 文件数据持久化
        this.persistence(fileDTO);
        LogUtils.info(log, "{} 模式上传文件成功", storageProperties.getType());
        return fileDTO;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean delete(DeleteRequestDTO requestDTO) {
        LogUtils.info(log, "{} 模式删除", storageProperties.getType());
        FileDTO file = fileService.getByOpenId(requestDTO.getOpenId());

        // 文件删除
        this.delete(file);
        // 删除数据库记录
        fileService.logicDelete(file);
        LogUtils.info(log, "{} 模式删除文件成功", storageProperties.getType());
        return Boolean.TRUE;
    }

    @Override
    public void download(DownloadRequestDTO requestDTO) {
        FileDTO file = fileService.getByOpenId(requestDTO.getOpenId());
        LogUtils.info(log, "{} 模式下载", storageProperties.getType());
        Long fileSize = file.getSize();
        try {
            HttpServletResponse response = HttpContextUtils.getResponse();
            assert response != null;
            ServletOutputStream outputStream = response.getOutputStream();

            // 获取 MimeType Files.probeContentType(path)
            response.setContentType(new MimetypesFileTypeMap().getContentType(file.getOriginalName()));
            response.setCharacterEncoding("UTF-8");
            // 下载文件
            if (Objects.equals(requestDTO.getAttachment(), Boolean.TRUE)) {
                response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + URLEncoder.encode(file.getOriginalName(), "UTF8"));
                response.setContentType("application/octet-stream; charset=UTF-8");
            } else {
                response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "filename=" + URLEncoder.encode(file.getOriginalName(), "UTF8"));
            }

            HttpServletRequest request = HttpContextUtils.getRequest();
            assert request != null;
            String rangeHeader = request.getHeader(HttpHeaders.RANGE);
            RangeParser.Range range = RangeParser.parse(rangeHeader, fileSize);

            InputStream inputStream;
            if (Objects.nonNull(range)) {
                long start = range.getStart();
                long end = range.getEnd();
                if (start > end) {
                    // 范围不合法
                    response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
                    LogUtils.error(log, "416 - 范围指定不合法: start: {}, end: {}", start, end);
                    return;
                } else {
                    inputStream = this.download(file, range);
                    response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
                    response.setContentLengthLong(end - start + 1);
                    response.addHeader(HttpHeaders.CONTENT_RANGE, String.format("bytes %d-%d/%d", start, end, fileSize));
                }
            } else {
                inputStream = this.download(file, null);
                response.setContentLengthLong(fileSize);
            }

            byte[] buffer = new byte[4096];
            int len;
            while ((len = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, len);
            }
            inputStream.close();
            outputStream.flush();
            outputStream.close();
        } catch (Exception e) {
            // 文件下载错误
            LogUtils.error(log, "文件下载错误: {}", e);
            BaseException.throwExp(ResourceMessage.DOWNLOAD_FILE_ERROR);
        }
        LogUtils.info(log, "{} 模式下载文件成功", storageProperties.getType());
    }

    /**
     * 文件下载
     *
     * @param fileDTO 文件信息
     * @param range   范围
     * @return 下载文件输入流
     * @throws Exception 文件下载异常
     */
    protected abstract InputStream download(FileDTO fileDTO, RangeParser.Range range) throws Exception;

    /**
     * 文件上传
     *
     * @param file    上传的文件
     * @param fileDTO 文件信息
     */
    protected abstract void upload(MultipartFile file, FileDTO fileDTO);

    /**
     * 文件删除
     *
     * @param file 要删除的文件
     */
    protected abstract void delete(FileDTO file);

    public FileDTO check(MultipartFile file, FileDTO fileDTO) {
        LogUtils.info(log, "文件上传检查");
        if (Objects.isNull(file)) {
            BaseException.throwExp(ResourceMessage.UPLOAD_FILE_EMPTY);
        }

        // 文件唯一性检查
        return fileService.getUniqueRecord(fileDTO);
    }

    /**
     * 持久化文件数据
     *
     * @param fileDTO 文件数据
     */
    private void persistence(FileDTO fileDTO) {
        LogUtils.info(log, "文件信息持久化");
        if (!fileService.insert(fileDTO)) {
            LogUtils.error(log, "数据库新增失败");
            BaseException.throwExp(BaseMessage.DATABASE_INSERT_ERROR);
        }
    }
}
